feat(core): GlobeView pointer-anchored zoom (wheel + transitions)#10307
feat(core): GlobeView pointer-anchored zoom (wheel + transitions)#10307charlieforward9 wants to merge 9 commits into
Conversation
Replace the `log.warn('around not supported in GlobeView')` no-op
with real spherical anchoring, mirroring the existing planar branch:
- `initializeProps`: when the start viewport is a GlobeViewport and the
screen anchor falls on the globe (`isPointOnGlobe`), unproject it to
lng/lat and stash it as `aroundLngLat`.
- `interpolateProps`: each frame, call `panByGlobeAnchor(aroundLngLat,
lerp(start.around, end.around, t))` so the geographic point stays
pinned under the anchor screen point during the transition.
This makes the `_onDoubleClick` zoom transition (`_getTransitionProps
({around: pos})`) actually anchor on GlobeView. Previously the warn
fired and the LERP ran without anchor maintenance, which read as a
center-anchored zoom-in regardless of where the user tapped.
Tests cover the on-globe anchored path and the off-globe fall-through.
- Consolidate stray imports at the top of the file. - Document the two GLOBE_ZOOM_ANCHOR_* constants so the empirical damping behavior (start damping at 0.75 of the limb, never below 35% strength) is self-explanatory. - Add a JSDoc to _getRayToGlobe explaining it as the shared ray/sphere math helper for unproject + isPointOnGlobe + panByGlobeAnchor.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Reviewed by Cursor Bugbot for commit 4ad99ed. Configure here.
| * `rotateSpeedX` (number) - speed of rotation using shift + left/right arrow keys, in degrees. Default `15`. | ||
| * `rotateSpeedY` (number) - speed of rotation using shift + up/down arrow keys, in degrees. Default `10`. | ||
| * `dragMode` (string) - drag behavior without pressing function keys, one of `pan` and `rotate`. | ||
| * `zoomAround` (`'center' | 'pointer'`) - zoom anchor mode when supported by the controller. Default depends on the controller. |
There was a problem hiding this comment.
I believe zoomAround: pointer is default behavior for most controllers - not sure if this is necessary.
| if (startPanCameraFrame !== undefined) s.startPanCameraFrame = startPanCameraFrame; | ||
| if (startPanAngularRate !== undefined) s.startPanAngularRate = startPanAngularRate; | ||
| if (startPanLockBearing !== undefined) s.startPanLockBearing = startPanLockBearing; | ||
| if (zoomAround !== undefined) s.zoomAround = zoomAround; |
There was a problem hiding this comment.
I was investigating the intermittent issue where pointer-anchored zoom stops working after hot reload. I think this may help:
- if (zoomAround !== undefined) s.zoomAround = zoomAround;
+ s.zoomAround = zoomAround || 'center';The problem might be that _shouldZoomAroundPointer() reads from _state, but the conditional guard means if the constructor is ever called without zoomAround in options (which can happen during HMR when props haven’t fully propagated yet), the key is just absent from state and the feature silently disengages.
To reproduce: set controller: {zoomAround: 'pointer'}, confirm it works, then trigger a hot reload.. it should intermittently reverts to center-zoom.
Unlike the other fields guarded by !== undefined (which are transient gesture anchors that are legitimately absent between interactions), zoomAround is a config option that always needs a value. Can you test it?
|
Superseded by the upstream-branch PR (moved off the fork per repo policy). |
…pout) Per #10307 review: zoomAround is a config option that always needs a value, unlike the transient gesture anchors guarded the same way. The !== undefined guard let a partial-props / HMR reconstruction drop the key, so _shouldZoomAroundPointer() saw undefined and silently reverted to center zoom.

Summary
Adds opt-in pointer-anchored zoom for
GlobeViewwhile preserving the existing center-anchored default.Latest stress pass:
5150bf4eefixes a sporadic continuous-zoom fallback wherezoomStartcould miss the globe, leavestartZoomLngLatempty, and then keep center-anchoring even after the pointer or touch centroid moved back over the globe. Pointer mode now recovers an anchor from the current zoom event and persists it for the rest of the gesture.Screen.Recording.2026-06-06.at.3.53.35.PM.mov
Changes
zoomAround?: 'center' | 'pointer'to controller options.GlobeViewport.panByGlobeAnchorfor spherical pointer anchoring during wheel, pinch, and double-click zoom.GlobeViewport.isPointOnGlobeandpanByGlobeAnchor.panByPositionwhenGlobeViewswitches toWebMercatorViewportat high zoom.aroundanchors whenzoomAround: 'pointer'is active.deck.setProps({controller: {zoomAround}}), ahead of persisted controller state.Validation
yarn vitest run --project headless test/modules/core/controllers/view-states.spec.tsyarn vitest run --project headless test/modules/core/controllers/controllers.spec.tsyarn vitest run --project headless test/modules/core/viewports/globe-viewport.spec.ts test/modules/core/transitions/linear-interpolator.spec.ts test/modules/core/controllers/controllers.spec.tsmodules/core/src/controllers/globe-controller.tsReview
Requested another pass from @chrisgervang to validate the additional stress coverage and the continuous pointer-zoom recovery path.
Merge Notes
Default remains
zoomAround: 'center', so existing GlobeView behavior is unchanged unless users opt in.